import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class BMP {

	class BMPFileHeader {
		byte magic_B;
		byte magic_M;
		int fileSize;
		short reserved1;
		short reserved2;
		int offset;
	}

	class BMPInfoHeader {
		int infoHeaderSize;
		int width;
		int height;
		short plances;
		short bitCount;
		int compressType;
		int imageSize;
		int xPixelsPerMeter;
		int yPixelsPerMeter;
		int colorUsed;
		int colorImportant;
	}

	enum COMPRESSION_TYPE {
		  BI_RGB,
		  BI_RLE8,
		  BI_RLE4,
		  BI_BITFIELDS,
		  BI_JPEG,  
		  BI_PNG
	};
		
	class RGBPixel {
		final byte red;
		final byte green;
		final byte blue;
		final byte alpha;
		
		RGBPixel(byte r, byte g, byte b) {
			this(r, g, b, (byte) 0x00);
		}
		
		RGBPixel(byte r, byte g, byte b, byte a) {
			this.red = r;
			this.green = g;
			this.blue = b;
			this.alpha = a;
		}
		
		public String toString() {
			return "R " + (int)(red & 0xFF) 
				+ " G " + (int)(green & 0xFF)
				+ " B " + (int)(blue & 0xFF) 
				+ " A " + alpha; 
		}
		
		public int toInt() {
			return (int) (((red & 0x000000FF) << 16)
				| ((green & 0x000000FF) << 8)
				| ((blue & 0x000000FF)));
		}
	}

	private BytesWriter writer;
	private BytesReader reader;
	private BMPFileHeader fileHeader;
	private BMPInfoHeader infoHeader;
	private RGBPixel[][] pixels;
	private RGBPixel[] pallete;
	
	public void read(InputStream in) throws IOException {
		reader = BytesReader.newBytesReader(in, false);
		this.readFileHeader();
		this.readInfoHeader();
		this.readPallete();
		this.readPixels();
	}
	
	public void copy(OutputStream out) throws IOException {
		writer = BytesWriter.newBytesWriter(out, false);
		this.writeFileHeader(this.fileHeader);
		this.writeInfoHeader(this.infoHeader);
		this.writePallete(this.pallete);
		this.writePixels();
	}
	
	private void readFileHeader() throws IOException {
		this.fileHeader = new BMPFileHeader();
		this.fileHeader.magic_B = reader.readByte();
		this.fileHeader.magic_M = reader.readByte();
		if (this.fileHeader.magic_B != 'B' || this.fileHeader.magic_M != 'M') {
			throw new RuntimeException("not a bitmap");
		}
		this.fileHeader.fileSize = reader.readInt();
		this.fileHeader.reserved1 = reader.readShort();
		this.fileHeader.reserved2 = reader.readShort();
		this.fileHeader.offset = reader.readInt();
	}
	
	private void writeFileHeader(BMPFileHeader fileHeader) throws IOException {
		if (fileHeader == null) {
			throw new RuntimeException("not found BMP file header");
		}
		writer.write(fileHeader.magic_B);
		writer.write(fileHeader.magic_M);
		writer.write(fileHeader.fileSize);
		writer.write(fileHeader.reserved1);
		writer.write(fileHeader.reserved2);
		writer.write(fileHeader.offset);
	}
	
	private void readInfoHeader() throws IOException {
		this.infoHeader = new BMPInfoHeader();
		this.infoHeader.infoHeaderSize = reader.readInt();
		this.infoHeader.width = reader.readInt();
		this.infoHeader.height = reader.readInt();
		this.infoHeader.plances = reader.readShort();
		this.infoHeader.bitCount = reader.readShort();
		this.infoHeader.compressType = reader.readInt();
		this.infoHeader.imageSize = reader.readInt();
		this.infoHeader.xPixelsPerMeter = reader.readInt();
		this.infoHeader.yPixelsPerMeter = reader.readInt();
		this.infoHeader.colorUsed = reader.readInt();
		this.infoHeader.colorImportant = reader.readInt();
	}
	
	private void writeInfoHeader(BMPInfoHeader infoHeader) throws IOException {
		if (infoHeader == null) {
			throw new RuntimeException("not found BMP info header");
		}
		writer.write(infoHeader.infoHeaderSize);
		writer.write(infoHeader.width);
		writer.write(infoHeader.height);
		writer.write(infoHeader.plances);
		writer.write(infoHeader.bitCount);
		writer.write(infoHeader.compressType);
		writer.write(infoHeader.imageSize);
		writer.write(infoHeader.xPixelsPerMeter);
		writer.write(infoHeader.yPixelsPerMeter);
		writer.write(infoHeader.colorUsed);
		writer.write(infoHeader.colorImportant);
	}

	private void readPallete() throws IOException {
		int colorCount = 0;
		if (this.infoHeader.colorUsed != 0) {
			colorCount = this.infoHeader.colorUsed;  
		} else {
			if (this.infoHeader.bitCount > 8) {
				return;
			}
			colorCount = 0x01 << this.infoHeader.bitCount;
		}
		if (colorCount <= 0) {
			return;
		}
		this.pallete = new RGBPixel[colorCount]; 
		for (int i = 0; i < colorCount; i++) {
			byte b = this.reader.readByte();
			byte g = this.reader.readByte();
			byte r = this.reader.readByte();
			byte a = this.reader.readByte();
			this.pallete[i] = new RGBPixel(r, g, b, a);
			System.err.println(this.pallete[i]);
		}
	}
	
	private void readPixels() throws IOException {
		int depth = this.infoHeader.bitCount;
		if (depth == 1 || depth == 2 || depth == 4) {
			// index to color table.
			return;
		}
		
		int w = this.infoHeader.width;
		int h = this.infoHeader.height;
		this.pixels = new RGBPixel[h][w];
		
		int pw = padding(w, depth);
		int padding = pw - w * depth / 8;
		
		if (depth == 8) {
			for (int i = h - 1; i >= 0; i--) {
				for (int j = 0; j < w; j++) {
					byte idx = reader.readByte();
					this.pixels[i][j] = new RGBPixel(idx, idx, idx);
				}
			}
		} else if (depth == 24) {
			for (int i = h - 1; i >= 0; i--) {
				for (int j = 0; j < w; j++) {
					if (j == w - 1 && padding > 0) {
						for (int p = 0; p < padding; p++) {
							reader.readByte();
						}
						break;
					}
					byte b = reader.readByte();
					byte g = reader.readByte();
					byte r = reader.readByte();
					this.pixels[i][j] = new RGBPixel(r, g, b);
				}
			}
		} else if (depth == 32) {
			for (int i = h - 1; i >= 0; i--) {
				for (int j = 0; j < w; j++) {
					byte b = reader.readByte();
					byte g = reader.readByte();
					byte r = reader.readByte();
					byte a = reader.readByte();
					this.pixels[i][j] = new RGBPixel(r, g, b, a);
				}
			}
		}
	}
	
	private int padding(int w, int depth) {
		return (w * depth + 31) / 32 * 4;
	}
	
	private void writePallete(RGBPixel[] pallete) throws IOException {
		if (pallete.length <= 0) {
			return;
		}
		for (RGBPixel pixel : pallete) {
			this.writer.write(pixel.blue);
			this.writer.write(pixel.green);
			this.writer.write(pixel.red);
			this.writer.write((byte) 0x00);
		}
	}
	
	private void writePixels() throws IOException {
		int depth = this.infoHeader.bitCount;
		if (depth == 1 || depth == 2 || depth == 4 || depth == 8) {
			// index to color table.
			return;
		}
		
		int w = this.infoHeader.width;
		int h = this.infoHeader.height;
		
		int pw = padding(w, depth);
		int padding = pw - w * depth / 8;
		
		if (depth == 24) {
			for (int i = h - 1; i >= 0; i--) {
				for (int j = 0; j < w; j++) {
					if (j == w - 1 && padding > 0) {
						for (int p = 0; p < padding; p++) {
							writer.write((byte) 0x00);
						}
						break;
					}
					RGBPixel pixel = this.pixels[i][j];
					System.err.println(this.pixels[i][j]);
					this.write(pixel);
				}
			}
		} else if (depth == 32) {
			for (int i = h - 1; i >= 0; i--) {
				for (int j = 0; j < w; j++) {
					RGBPixel pixel = this.pixels[i][j];
					System.err.println(this.pixels[i][j]);
					this.write(pixel);
				}
			}
		} 
	}
	
	private void write(RGBPixel pixel) throws IOException {
		if (this.infoHeader.bitCount == 24) {
			writer.write(pixel.blue);
			writer.write(pixel.green);
			writer.write(pixel.red);
		} else if (this.infoHeader.bitCount == 32) {
			writer.write(pixel.blue);
			writer.write(pixel.green);
			writer.write(pixel.red);
			writer.write(pixel.alpha);
		}
	}
	
	private void writeGray(RGBPixel pixel) throws IOException {
		int b = pixel.blue;
		int g = pixel.green;
		int r = pixel.red;
		int gray = (int) Math.round(r * 0.3 + g * 0.59 + b * 0.11);
		writer.write(gray);
	}
	
	public void saveAsGray(OutputStream out) throws IOException {
		writer = BytesWriter.newBytesWriter(out, false);
		this.writeFileHeader(this.fileHeader);
		
		BMPInfoHeader newInfoHeader = new BMPInfoHeader();
		newInfoHeader.infoHeaderSize = reader.readInt();
		newInfoHeader.width = reader.readInt();
		newInfoHeader.height = reader.readInt();
		newInfoHeader.plances = reader.readShort();
		newInfoHeader.bitCount = 8;
		newInfoHeader.compressType = reader.readInt();
		newInfoHeader.imageSize = reader.readInt();
		newInfoHeader.xPixelsPerMeter = reader.readInt();
		newInfoHeader.yPixelsPerMeter = reader.readInt();
		newInfoHeader.colorUsed = reader.readInt();
		newInfoHeader.colorImportant = reader.readInt();
		this.writeInfoHeader(newInfoHeader);
		
		this.writePallete(this.pallete);
		this.writePixelsAsGray();
	}
	
	private void writePixelsAsGray() throws IOException {
		int depth = 32;
		if (depth == 1 || depth == 2 || depth == 4 || depth == 8) {
			return;
		}
		
		int w = this.infoHeader.width;
		int h = this.infoHeader.height;
		
		int pw = padding(w, depth);
		int padding = pw - w * depth / 8;
		
		if (depth == 24) {
			for (int i = h - 1; i >= 0; i--) {
				for (int j = 0; j < w; j++) {
					if (j == w - 1 && padding > 0) {
						for (int p = 0; p < padding; p++) {
							writer.write((byte) 0x00);
						}
						break;
					}
					RGBPixel pixel = this.pixels[i][j];
					System.err.println(this.pixels[i][j]);
					this.writeGray(pixel);
				}
			}
		} else if (depth == 32) {
			for (int i = h - 1; i >= 0; i--) {
				for (int j = 0; j < w; j++) {
					RGBPixel pixel = this.pixels[i][j];
					System.err.println(this.pixels[i][j]);
					this.writeGray(pixel);
				}
			}
		} 
	}

	public BufferedImage createPalleteBufferedImage() {
		if (this.infoHeader.bitCount != 8)
			return null;
		
		int w = this.infoHeader.width;
		int h = this.infoHeader.height;
		
		int d = w / 32 + 1;
		if (d == 0)
			d = 1;
		
		int iw = w + d;
		int dh = d * 9; 
		int ih = h + dh ;
		BufferedImage bimage = new BufferedImage(iw, ih, BufferedImage.TYPE_INT_RGB);

		for (int i = 0; i < 32; i++) {
			for (int j = 0; j < 8; j++) {
				int rgb = this.pallete[i * 8 + j].toInt();
				createPalleteBufferedImage(i * d, j * d, d, rgb, bimage);
			}
		}
		
		for (int i = 0; i < w; i++) {
			for (int j = h - 1; j >= 0; j--) {
				int idx = (int) (this.pixels[j][i].red & 0x00FF);
				bimage.setRGB(i, j + dh, this.pallete[idx].toInt());
			}
		}
		
		return bimage;
	}
	
	private void createPalleteBufferedImage(int x, int y, int w, int rgb, BufferedImage bimage) {
		for (int i = 0; i < w; i++) {
			for (int j = 0; j < w; j++) {
				bimage.setRGB(x + i, y + j, rgb);
			}
		}
	}
	
	public BufferedImage createBufferedImage() {
		int w = this.infoHeader.width;
		int h = this.infoHeader.height;

		BufferedImage bimage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
		for (int i = 0; i < w; i++) {
			for (int j = h - 1; j >= 0; j--) {
				bimage.setRGB(i, j, this.pixels[j][i].toInt());
			}
		}
		
		return bimage;
	} 
}
